#!/usr/bin/env python
#
#       Game.py
#       
#       Copyright 2009 Ben Davis <its.hip@gmail.com>
#       
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#       
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#       
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.

import math
import os
import pygame

import Effects
import Events
import Graphics
import Interfaces
import Objects
import Options
import Players
import ToServer

from Client import Client
client = Client()

class Game(Interfaces.Absolutes):
    def __init__(self, parent):
        list.__init__(self)
        self.parent = parent
        # Game widget will always size itself to its parent and return position (0, 0)
        # Position in self.rect is used internally
        # To draw it smaller than the screen size, put it in a container and control its size
        self.rect = pygame.Rect((0, 0), self.parent.rect.size)
        self.surface = pygame.Surface(self.rect.size).convert()
        # Create a counter to control the timing of the selected ship's "flash"
        self.selected_counter = 0
        self.selected_counter_max = 20
        self.target_graphic = client.tile_graphics[os.path.join("Data", "Tiles", "Target")]
        self.path_graphic = client.tile_graphics[os.path.join("Data", "Tiles", "MovePath")]
        # Create a list to store where we need to pan to
        self.destinations = []
        self.players = Players.Players()
        self.cursor = Cursor()
        # Selected ship
        self.selected = None
        
        self.clouds = []
        self.effects = Effects.EffectsHandler()
        self.night = 20 # how dark is it? 255  = er... cant see shit. 0 = day.
        # !!!
        # 85 is the point that lights need to go on and off!
        # !!!
        self.night_surface = pygame.Surface(pygame.display.list_modes()[0])
        self.night_colours = {  0 : (252, 159,  28),
                               20 : (190,  76,  18),
                               50 : ( 14,  82, 125),
                              150 : (  7,  36,  53),
                              180 : (  6,   9,  56),
                              220 : (  3,   8,  32),
                              255 : (  0,   0,   0)}
        
        self.events = Events.Events()
        for b in range(4, 8):
            self.events.bind(self.mouse_scroll, type=pygame.MOUSEBUTTONDOWN, button=b)
        self.events.bind(self.left_mouse_button_up, type=pygame.MOUSEBUTTONUP, button=1)
        self.events.bind(self.cursor.move,  0,  1, type=pygame.KEYDOWN, key=pygame.K_w)
        self.events.bind(self.cursor.move,  0, -1, type=pygame.KEYDOWN, key=pygame.K_a)
        self.events.bind(self.cursor.move, -1,  0, type=pygame.KEYDOWN, key=pygame.K_q)
        self.events.bind(self.cursor.move,  1,  0, type=pygame.KEYDOWN, key=pygame.K_s)
        #self.events.bind(lambda self.night: self.night = self.night + amount,  5, type=pygame.KEYDOWN, key=pygame.K_e)
        #self.events.bind(lambda self.night: self.night = self.night + amount,  -5, type=pygame.KEYDOWN, key=pygame.K_d)
        self.events.bind(client.server.send, ToServer.EndTurn(), type=pygame.KEYDOWN, key=pygame.K_s)
        self.events.bind(self.cursor.select, type=pygame.KEYDOWN, key=pygame.K_SPACE)
        self.events.bind(self.zoom, -1, type=pygame.KEYDOWN, key=pygame.K_i)
        self.events.bind(self.zoom, 1, type=pygame.KEYDOWN, key=pygame.K_k)
        self.events.bind(self.fire_weapon, 0, type=pygame.KEYDOWN, key=pygame.K_z)
        self.events.bind(self.fire_weapon, 1, type=pygame.KEYDOWN, key=pygame.K_x)
        self.events.bind(self.text_input, type=pygame.KEYDOWN, key=pygame.K_t)
        self.events.bind(self.deselect, type=pygame.KEYDOWN, key=pygame.K_ESCAPE)
                
    def draw(self):
        # Clear the surface
        self.surface.fill((0, 0, 0))
        
        # Draw map
        self.map.draw()
        
        # Draw the current weapon's targets
        try:
            for direction in self.targets:
                for x, y in direction:
                    target = self.selected.get_tile_direction_relative(x, y, 0)
                    self.blit((self.target_graphic[Options.ZOOM_LEVEL], pygame.Rect(self.shadow_location(target), self.target_graphic[Options.ZOOM_LEVEL].get_size())))
        except AttributeError:
            pass
            
        # Draw all entities
        for entitity in self.entities():
            self.blit(entitity.draw())
            
        # Draw 'lower' effects
        for effect in self.effects.lower:
            self.blit(effect.draw())
            
        
        # If self.night == 0 then we don't need to blit it anyway
        if self.night > 0:
            # Fill night overlay with colour
            keys = sorted(self.night_colours.keys())
            for i in range(len(keys)):
                if self.night < keys[i]:
                    top = keys[i]
                    bottom = keys[i - 1]
                    max = top - bottom
                    percentage = float(self.night - bottom) / max
                    colour = []
                    for j in range(3):
                        colour.append(int(float(self.night_colours[bottom][j]) * (1 - percentage) + float(self.night_colours[top][j]) * percentage))
                    self.night_surface.fill(colour)
                    break
                # If we're dead on a colour
                elif self.night == keys[i]:
                    # Just fill
                    self.night_surface.fill(self.night_colours[keys[i]])
                    break
            
            # Set alpha
            self.night_surface.set_alpha(self.night)
            # Draw night overlay
            self.surface.blit(self.night_surface, (0, 0))
        
        # Draw in ship lights
        if self.night > 85:
            for player in self.players.values():
                for ship in player.ships.values():
                    # we're doing lights this time, second subscript == 1
                    self.blit(ship.draw_lights())
                    
                    
        # Draw selected path
        # If we have a ship selected
        if self.selected:
            # If the player is drawing a path
            try:
                for tile in self.selected.path:
                    self.blit((self.path_graphic[Options.ZOOM_LEVEL], pygame.Rect(self.shadow_location(tile), self.path_graphic[Options.ZOOM_LEVEL].get_size())))
            except AttributeError:
                # Draw possible paths.
                # But only if it's our turn
                if self.players.turn == self.player.key():
                    ship = self.selected
                    # And only if it's our ship
                    if ship.player == self.player.key():
                        for r in range(-ship.r_remaining, ship.r_remaining + 1):
                            if ship.steam - abs(r) < ship.move_remaining:
                                    y = ship.steam - abs(r)
                            else:
                                    y = ship.move_remaining
                            y += 1
                            if y > 1:
                                for x in range(1, y):
                                    # Get the tile position to draw
                                    tile = ship.get_tile_direction_relative(0, x, r)
                                    # If the tile is on the map
                                    if tile[0] >= 0 and tile[1] >= 0 and tile[0] < self.map.width and tile[1] < self.map.height:
                                        # Create the sprite to draw
                                        temp = self.path_graphic[Options.ZOOM_LEVEL].copy()
                                        temp.fill((0, 200, 0, 150 - 150 * x / y), None, pygame.BLEND_RGBA_MULT)
                                        # Draw it on the screen
                                        self.blit((temp, pygame.Rect(self.shadow_location(tile), temp.get_size())))
                
        # If we have selected a ship
        if self.selected and not self.selected_counter / self.selected_counter_max:
            # If this is our ship
            if self.selected in self.player.ships.values():
                # Highlight in white
                colour = (255, 255, 255)
            # Otherwise, it's an enemy
            else:
                # Highlight in red
                colour = (255, 0, 0)
            surface, position = self.selected.draw()
            # Create a copy of it's sprite
            mask = surface.copy()
            # Create a white mask of the non-transparent areas
            mask.fill(colour, None, pygame.BLEND_ADD)
            # Lower the alpha levels
            mask.fill(colour + (Options.HIGHLIGHT_ALPHA,), None, pygame.BLEND_RGBA_MULT)
            # Blit the highlight to screen
            self.blit((mask, position))
            
        for effect in self.effects.higher:
            self.blit(effect.draw())
        
        self.selected_counter = (self.selected_counter + 1) % (self.selected_counter_max * 2)
        
        # Draw child widgets
        for widget in self:
            #surface, position = widget.draw()
            self.blit(widget.draw())
        return self.surface, pygame.Rect((0, 0), self.rect.size)
        
    def update(self):
        # Try to pan to the next destination in the list
        try:
            self.rect.center = self.destinations.pop(0)
        except IndexError:
            pass
            
        # Work-around for multiple arrow-keys, not sure it will play
        # nicely with low framerate
        
        # Get the keys currently being pressed
        keys = pygame.key.get_pressed()
        if keys[pygame.K_UP]:
            self.pan_by(0, - (Options.KEY_SCROLL * Options.ZOOM_MOD[Options.ZOOM_LEVEL]))
        if keys[pygame.K_DOWN]:
            self.pan_by(0, + (Options.KEY_SCROLL * Options.ZOOM_MOD[Options.ZOOM_LEVEL]))
        if keys[pygame.K_LEFT]:
            self.pan_by(- (Options.KEY_SCROLL * Options.ZOOM_MOD[Options.ZOOM_LEVEL]), 0)
        if keys[pygame.K_RIGHT]:
            self.pan_by(+ (Options.KEY_SCROLL * Options.ZOOM_MOD[Options.ZOOM_LEVEL]), 0)
            
        for player in self.players.values():
            for ship in player.ships.values():
                ship.poll()
            
        self.effects.poll()
        
        # Update child widgets
        for widget in self:
            widget.update()
    
    def clicking(self, pos):
        x, y = pos
        x += self.rect.left
        y += self.rect.top
        pos = x, y
        for widget in self:
            clicking = widget.clicking(pos)
            if clicking:
                return clicking
        if self.rect.collidepoint(pos):
            self.click(pos)
            return self
    
    def click(self, pos):
        if self.cursor.move_screen_xy(pos):
            # If player is holding shift key
            if pygame.key.get_mods() & pygame.KMOD_LSHIFT == pygame.KMOD_LSHIFT \
            or pygame.key.get_mods() & pygame.KMOD_RSHIFT == pygame.KMOD_RSHIFT:
                # If it's this player's turn
                if self.player.key() == self.players.turn:
                    try:
                        # If it's this player's ship
                        if self.selected.player == self.player.key():
                                steam = 0
                                x, y = self.map.map_location(event.pos)
                                r = self.selected.get_direction_to_tile(x, y)
                                angle = r - self.selected.r
                                if angle > 0:
                                    if angle > 4:
                                        # Turn port
                                        angle = self.selected.r - r
                                        while angle > 0:
                                            angle += -1
                                            steam += 1
                                    else:
                                        # Turn starboard
                                        while angle > 0:
                                            angle += -1
                                            steam += 1
                                if self.selected.steam >= steam:
                                    client.server.send(ToServer.Rotate(self.selected, r))
                    except (TypeError, AttributeError):
                        pass
            # If player isn't holding shift key
            else:
                # Perform a select on tile
                if self.cursor.select():
                    # If it's this player's turn
                    if self.player.key() == self.players.turn:
                        # If it's this player's ship
                        if self.selected.player == self.player.key():
                            self.selected.path = [self.cursor.pos()]
        return self
                        
    def hovering(self, pos):
        x, y = pos
        x += self.rect.left
        y += self.rect.top
        pos = x, y
        for widget in self[:]:
            # Remove all Tooltip widgets
            if isinstance(widget, Interfaces.Tooltip):
                self.remove(widget)
        try:
            del(self.targets)
        except AttributeError:
            pass
        for widget in self:
            hovering = widget.hovering(pos)
            if hovering:
                return hovering
        if self.rect.collidepoint(pos):
            self.hover(pos)
            return self
            
    def hover(self, pos):
        # Move the cursor to mouse position
        self.cursor.move_screen_xy(pos)
        try:
            # If there isn't something in the way
            if not self.map.get_at_screen_xy(pos):
                # If tile isn't already in the path
                if not self.cursor.pos() in self.selected.path:
                    x, y = self.selected.path[-1]
                    if abs(self.cursor.x - x) <= 1 \
                    and abs(self.cursor.y - y) <= 1:
                        try:
                            x, y = self.selected.path[-2]
                            # If going through a diagonal
                            if abs(self.cursor.x - x) == 1 \
                            and abs(self.cursor.y - y) == 1:
                                # Delete the last position
                                del(self.selected.path[-1])
                        except IndexError:
                            pass
                        # Add the current position
                        self.selected.path.append(self.cursor.pos())
                        steam = 0
                        x, y, r = self.selected.pos()
                        path_x, path_y, path_r = self.selected.pos()
                        for x, y in self.selected.path[1:]:
                            r = self.selected.get_direction_to_tile_relative(x - path_x, y - path_y)
                            angle = r - path_r
                            if angle > 0:
                                if angle > 4:
                                    # Turn port
                                    angle = path_r - r
                                    while angle > 0:
                                        angle += -1
                                        steam += 1
                                else:
                                    # Turn starboard
                                    while angle > 0:
                                        angle += -1
                                        steam += 1
                            steam += 1
                            path_x, path_y, path_r = x, y, r
                        if steam > self.selected.steam:
                            del(self.selected.path[-1])
            try:
                self.append(Interfaces.Tooltip(self, "SU: " + str(steam), pos))
            except UnboundLocalError:
                pass
        except (AttributeError, TypeError):
            pass
        return self
    
    def entities(self):
        r = [self.cursor]
        for player in self.players.values():
            for ship in player.ships.values():
                r.append(ship)
        r += self.clouds
        return r
        
    def blit(self, (surface, position)):
        # If the sprite is on screen
        if position.colliderect(self.rect):
            self.surface.blit(surface, position.move(-self.rect.x, -self.rect.y))
            
    def pan_by(self, x_amount, y_amount):
        self.rect.move_ip(x_amount, y_amount)
        
    def pan_to(self, x, y, step):
        """Pans the screen to x, y with a pre-defined 'step';
        i.e. the maximum distance the screen can pan per frame"""
        
        if step > 0:
            # Calculate the difference between the two points
            x_dif = x - self.rect.centerx + Options.TILE_WIDTH[Options.ZOOM_LEVEL] / 2
            y_dif = y - self.rect.centery + Options.TILE_HEIGHT[Options.ZOOM_LEVEL] / 2
            # Calculate the distance
            dist = distance(self.rect.center, (x, y))
            steps = math.ceil(dist / step)
            # If we're not already where we want to be
            if steps > 0:
                # Calculate the increments for each frame
                x_step = x_dif / steps
                y_step = y_dif / steps
                # Initialise cx and cy to the centre of the screen
                cx, cy = self.rect.center
                # For each frame
                for i in range(steps):
                    # Transform the position by x_step, y_step
                    cx += x_step
                    cy += y_step
                    self.destinations.append((cx, cy))
        # If step <= 0
        else:
            # Then we just set the position straight away
            self.rect.center = x, y
        
    def pan_to_tile(self, x, y, step):
        # here for planning purposes
        # pan to a tile over time
        pass
        
    def pan_to_ship(self, ship, step):
        # here for planning purposes 
        # pan to a ship over time. reads from the players own ships.
        x, y = ship.shadow_location()
        self.pan_to(x, y, step)

    def zoom(self, event, level):
        # Get the current distance of rect from (0, 0)
        centre_x, centre_y = self.rect.center
        # Account for the current zoom level
        centre_x, centre_y = centre_x * (2 ** Options.ZOOM_LEVEL), centre_y * (2 ** Options.ZOOM_LEVEL)
        # Set the new zoom level
        Options.ZOOM_LEVEL += level
        if Options.ZOOM_LEVEL < 0:
            Options.ZOOM_LEVEL = 0
        if Options.ZOOM_LEVEL > 2:
            Options.ZOOM_LEVEL = 2
        # Set the new view centre based on new zoom level
        self.rect.center = centre_x / (2 ** Options.ZOOM_LEVEL), centre_y / (2 ** Options.ZOOM_LEVEL)

    def shadow_location(self, (x, y)):
        return (x - y + self.map.height - 1) * 0.5 * Options.TILE_WIDTH[Options.ZOOM_LEVEL], (x + y) * 0.5 * Options.TILE_HEIGHT[Options.ZOOM_LEVEL]

    def screen_location(self, (x, y)):
        x, y = self.shadow_location((x, y))
        x += self.xy_mod.x.value()
        y += self.xy_mod.y.value()
        return (x, y - Options.TILE_HEIGHT[Options.ZOOM_LEVEL])
    
    def select(self, pos):
        x, y = pos[0], pos[1]
        old = self.selected
        self.deselect()
        for player in self.players.values():
            # For each ship in each player
            for ship in player.ships.values():
                # If ship at x, y
                if ship.x == x and ship.y == y:
                    # Select the ship
                    self.selected = ship
                    ship.select()
                    if Options.SHIPINFO_ON_SELECT:
                        for widget in self:
                            # For each ShipInfo widget
                            if isinstance(widget, Interfaces.ShipInfoContainer):
                                # If we already have a ShipInfo widget for the selected ship
                                if widget.ship == self.selected:
                                    # Don't make a new one
                                    self.remove(widget)
                        # Create one
                        self.append(Interfaces.ShipInfoContainer(self, self.selected))
                    # Reset the select counter (makes cursor flash)
                    self.selected_counter = 0
                    #if old == self.selected:
                        #self.view.pan_to_ship(self.selected, 20)
                    # Return True if we selected a ship
                    return True
        # Return False if there was no ship to select
        return False
    
    def deselect(self, *args):
        # If we have selected a ship
        if self.selected:
            for widget in self[:]:
                # Remove any Weapon widgets
                if isinstance(widget, Interfaces.Weapon):
                    self.remove(widget)
            if Options.SHIPINFO_ON_SELECT:
                for widget in self:
                    # For each ShipInfo widget
                    if isinstance(widget, Interfaces.ShipInfoContainer):
                        # If the widget's ship is the selected ship
                        if widget.ship == self.selected:
                            # If we're not hovering over the widget and the player wants ShipInfo on hover
                            if not (Options.SHIPINFO_ON_HOVER and self.cursor.pos() == (widget.ship.x, widget.ship.y)):
                                # Delete the ShipInfo widget
                                self.remove(widget)
            # Deselect the ship
            self.selected.deselect()
            self.selected = None
            return True
        return False
    
    def mouse_scroll(self, event):
        if event.button == 4:
            self.pan_by(0, + (Options.MOUSE_SCROLL * Options.ZOOM_MOD[Options.ZOOM_LEVEL]))
        if event.button == 5:
            self.pan_by(0, - (Options.MOUSE_SCROLL * Options.ZOOM_MOD[Options.ZOOM_LEVEL]))
        if event.button == 6:
            self.pan_by( + (Options.MOUSE_SCROLL * Options.ZOOM_MOD[Options.ZOOM_LEVEL]), 0)
        if event.button == 7:
            self.pan_by( - (Options.MOUSE_SCROLL * Options.ZOOM_MOD[Options.ZOOM_LEVEL]), 0)
    
    def left_mouse_button_up(self, event, *args):
        try:
            if len(self.selected.path) > 1:
                client.server.send(ToServer.Move(self.selected, self.selected.path[1:]))
            else:                        
                if Options.PAN_ON_SELECT:
                    self.pan_to_ship(self.selected, 30)
            del(self.selected.path)
        except AttributeError:
            try:
                if Options.PAN_ON_SELECT:
                    self.pan_to_ship(self.selected, 30)
            except AttributeError:
                pass

    def fire_weapon(self, weapon, *args):
        # If we have a selected ship
        if self.selected:
            client.server.send(ToServer.Fire(self.selected, weapon))
            
    def text_input(self, *args):
        # Create a new TextInput and parent for typing into                        
        saying = Interfaces.Horizontal(client, (0, 0, 0, 100))
        saying.append(Interfaces.Text(saying, "Say:"))
        saying.append(Interfaces.MessageInput(saying, client.server))
        saying.rect.move_ip(100, 100)
        client.append(saying)
        # Set the input focus to the new TextInput
        client.focus(saying[1])
            
class Cursor(Objects.Object):
    def __init__(self, x=0, y=0):
        Objects.Object.__init__(self, x, y, 0)
        self.x = x
        self.y = y
        self.textures = client.tile_graphics[os.path.join('Data', 'Tiles', 'Cursor')]
        
    def draw(self):
        return self.textures[Options.ZOOM_LEVEL], pygame.Rect(self.shadow_location(), (Options.SPRITE_SIZE[Options.ZOOM_LEVEL], Options.SPRITE_SIZE[Options.ZOOM_LEVEL]))
        
    def select(self):
        return client.game.select(self.pos())
    
    def move_xy(self, x, y):
        """Move cursor to absolute map (x, y)."""
        self.x = x
        if self.x < 0:
            self.x = 0
        elif self.x > (client.game.map.width - 1):
            self.x = client.game.map.width - 1
        self.y = y
        if self.y < 0:
            self.y = 0
        elif self.y > (client.game.map.height - 1):
            self.y = client.game.map.height - 1
        if Options.SHIPINFO_ON_HOVER:
            # Get the ship under cursor
            ship = client.game.map.get_at_xy(self.pos())
            for widget in client.game:
                # If there's a ShipInfo widget already
                if isinstance(widget, Interfaces.ShipInfoContainer):
                    # If it's for the ship currently at the cursor
                    if widget.ship == ship:
                        # Just leave it there
                        return
                    # If player wants ShipInfo on selected ships and
                    # this widget's ship is selected then don't delete
                    if not (Options.SHIPINFO_ON_SELECT and widget.ship == client.game.selected):
                        client.game.remove(widget)
            # If we have a ship under the cursor
            if ship:
                # Make a ShipInfo widget for it
                client.game.append(Interfaces.ShipInfoContainer(client.game, ship))
    
    def move(self, x, y, *args):
        """Move cursor by (x, y) relative to current position."""
        self.move_xy(self.x + x, self.y - y)
            
    def move_screen_xy(self, pos):
        """Translate screen (x, y) to map (x, y) and move cursor there.
        
        If screen (x, y) is on map, move cursor and return True;
        Otherwise, return False
        """
        x, y = client.game.map.map_location(pos)
        # If map (x, y) exists on map
        if x <= client.game.map.width and \
        x >= 0 and \
        y <= client.game.map.height and \
        y >= 0:
            # Move cursor
            self.move_xy(x, y)
            return True
        # If map (x, y) off the map
        return False
    
    def pos(self):
        return self.x, self.y
        
def distance(start, end):
    """Returns the pythagorean distance between two points"""
    x1, y1 = start
    x2, y2 = end
    return math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
